Skip to content

feat: server delivery SBOM — layered scan, --merge, rootfs UI input#161

Merged
haksungjang merged 3 commits into
mainfrom
feat/server-delivery-sbom
Jun 17, 2026
Merged

feat: server delivery SBOM — layered scan, --merge, rootfs UI input#161
haksungjang merged 3 commits into
mainfrom
feat/server-delivery-sbom

Conversation

@haksungjang

Copy link
Copy Markdown
Member

배경

CentOS 기반 서버를 납품하는 공급사의 서버 SBOM 제출을 지원한다. 서버 SBOM은 세 층(OS rootfs, 애플리케이션, 정적 링크 의존성)으로 나뉘는데, 한 층만 스캔하면 나머지가 빠져 반려된다. 도구·문서에 두 빈틈이 있었다: 층별 SBOM을 합치는 기능 부재, UI에서 임의 rootfs 디렉터리를 스캔할 수단 부재.

변경

scanner — --merge

  • CycloneDX SBOM 2개 이상을 purl 기준으로 dedupe 병합하고, 최상위 컴포넌트를 --project/--version으로 기재.
  • 각 컴포넌트에 bomlens:layer 속성으로 출처(층)를 보존. dependencies 트리는 bom-ref 충돌로 병합하지 않음.
  • 컴포넌트를 argv가 아니라 파일(jq -s/--slurpfile)로 전달해 실제 서버 규모(수백~수천 컴포넌트)에서 ARG_MAX를 넘지 않음.
  • ROOTFS 스캔의 최상위 컴포넌트를 stamp: syft가 dir: 스캔을 마운트 경로(/target)로 명명해 내부 경로가 노출되던 문제를 --project 값으로 교정.

ui — rootfs-dir 입력

  • 웹 UI에 "디렉터리 경로" 입력을 추가해 /src 하위 rootfs나 임의 하위 폴더를 ROOTFS 모드로 스캔.
  • safe_scan_dir()가 realpath + /src 경계 강제로 ../ traversal·절대경로·심볼릭 링크 탈출을 차단. 허용 경계는 목록이라 후일 --mount 확장이 쉬움.

docs — 서버 납품 가이드

  • docs/guides/server-delivery.{md,ko.md} 신규. 층별 분리 제출을 기본으로, 병합은 제출 시스템이 단일 BOM을 요구할 때의 선택 단계로 안내. 정적 링크 한계와 반려 사유 포함.
  • cli.{md,ko.md}--merge 문서화, --target 설명 확장. by-input 상호 링크, mkdocs nav 등록.

검증

실제 rpm rootfs(UBI9) 대상으로 scan-sbom.sh CLI end-to-end 실측.

  • OS층: 111개 컴포넌트 중 110개가 표준 pkg:rpm purl.
  • 병합: 실제 --merge로 128개(rpm 110 + apk 16) 병합, 층 라벨 mms-relay-os/alpine 보존.
  • 경계·에러 4종(파일 1개, --target 동시, 없는 파일, CycloneDX 아님) 모두 정상.
  • 이 과정에서 ARG_MAX 초과와 /target 경로 노출 두 버그를 발견·수정.
  • merge-sbom.sh/stamp-metadata.sh ShellCheck 통과, 프론트엔드 tsc/vite build 통과, server.py py_compile 통과.

Add a --merge mode that combines two or more CycloneDX SBOMs into one,
dedupes components by purl, and stamps the root component with
--project/--version. It is built for layered server delivery: an OS
rootfs layer, an application layer, and a static-link layer are each
scanned separately, then merged when a submission system needs a single
product BOM. Each component keeps a bomlens:layer property so layer
provenance survives the merge; dependencies trees are dropped (their
bom-ref namespaces collide across inputs).

merge-sbom.sh passes components between jq steps via files (jq -s /
--slurpfile), not argv, so a real server SBOM with hundreds/thousands of
components does not overflow ARG_MAX.

Also stamp the ROOTFS root component: syft names a dir: scan after the
scan path (/target), which is meaningless and leaks the container mount
path into the SBOM. ROOTFS now gets the caller's --project/--version,
the same fix stamp-metadata.sh already applied to cdxgen scans.
Add a "Directory path" (rootfs-dir) input to the web UI so an OS rootfs
or any subfolder under the launch folder can be scanned as a directory
(MODE=ROOTFS), not just the fixed current folder.

safe_scan_dir() resolves the user path with realpath and forces it
inside /src (the only mounted root), rejecting ../ traversal, absolute
paths, and symlink escapes. The allowed-root boundary is a list, so a
future `--ui --mount <host-path>` can extend it without touching the
check.
Document how a supplier builds a server SBOM: scan the OS rootfs, the
application, and the static-link dependencies as three layers, verify
each, and submit them separately. Submitting the layers separately is
the default — it keeps each layer reviewable and preserves per-layer
dependency graphs. Merging into one product BOM with --merge is an
optional step for when a submission system requires a single file.

Also note the static-link detection limits (binary/firmware mode plus
hand-recorded build sources, with BDBA as SKT's complementary check) and
the rejection causes (hand-written SBOMs, pkg:generic, raw-directory
scans, scanning before the build). Document --merge in the CLI reference
and expand --target to mention rootfs/staging targets.
Comment thread docker/web/server.py
real = os.path.realpath(os.path.join(SRC_DIR, rel))
for root in ALLOWED_SCAN_ROOTS:
r = os.path.realpath(root)
if (real == r or real.startswith(r + os.sep)) and os.path.isdir(real):
@haksungjang haksungjang merged commit 324cd1d into main Jun 17, 2026
23 of 24 checks passed
@haksungjang haksungjang deleted the feat/server-delivery-sbom branch June 17, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants